# Snow CLI User Guide - Custom StatusLine

## Overview

Snow CLI supports loading custom StatusLine plugins from your user directory. You can place one or more JavaScript files in `~/.snow/plugin/statusline/`, and Snow CLI will load them on startup.

Use this feature when you want to:

- Show your own environment status
- Display project-specific hints
- Add time, directory, branch, service, or local machine indicators
- Switch status text by Simplified Chinese, Traditional Chinese, and English
- Override a built-in StatusLine plugin with your own implementation

## Plugin Directory

Snow CLI currently loads StatusLine plugins from:

```bash
~/.snow/plugin/statusline/
```

Supported file extensions:

- `.js`
- `.mjs`
- `.cjs`

Notes:

- Plugins are loaded from the user directory only
- Snow CLI sorts plugin files by filename before loading
- Adding, modifying, or deleting plugin files hot-reloads automatically — no Snow CLI restart required

## Export Formats

A plugin module can export in any of these forms:

```js
export default { ... }
```

```js
export const statusLineHook = { ... }
```

```js
export const statusLineHooks = [{ ... }, { ... }]
```

If multiple plugins use the same hook `id`, the later loaded plugin overrides the earlier one.

## Hook Structure

Each StatusLine hook uses this structure:

```js
export default {
	id: 'custom.example',
	refreshIntervalMs: 60000,
	getItems(context) {
		return {
			id: 'custom-example-item',
			text: 'Hello',
			detailedText: 'Hello from custom status line',
			color: 'cyan',
			priority: 200,
		};
	},
};
```

Field description:

- `id`: unique hook id, used for merging and override behavior
- `refreshIntervalMs`: optional refresh interval in milliseconds; minimum effective interval is 1000 ms
- `enable`: optional, whether to enable this hook, defaults to `true`, set to `false` to temporarily disable
- `getItems(context)`: returns one item, multiple items, or `undefined`

The `getItems` result supports:

- single item object
- array of item objects
- `undefined` or `null` to render nothing
- async return values via `async getItems()`

## Render Item Fields

Each render item supports the following fields:

- `id`: optional item id; Snow CLI auto-generates one if omitted
- `text`: short text used in simple mode
- `detailedText`: optional text used in normal mode; falls back to `text`
- `color`: optional Ink color string or hex color
- `gradient`: optional gradient color array, containing two or more hex colors or named colors (e.g. `['#10B981', '#60A5FA']`). When provided, the text is rendered character-by-character with interpolated gradient colors; falls back to `color` when not provided or invalid
- `priority`: optional sort priority; lower values render first

### Gradient Colors

Terminals do not support pixel-level gradients. Snow CLI simulates gradients by interpolating colors character by character. When you provide a `gradient` array, Snow CLI generates a linear interpolation color sequence based on the text length, assigning one color per character.

Using 2 to 5 colors is recommended:

- 2 colors: left-right two-tone, suitable for very short text
- 3-4 colors: natural gradient transition, recommended for 6-15 character text
- 5 colors: the upper limit, best for longer text

`gradient` takes priority over `color`: when both are provided, `gradient` is used; if `gradient` is invalid, it falls back to `color`.

## Context Object

`getItems(context)` receives this context object:

```js
{
	cwd: '/absolute/current/working/directory',
	platform: 'darwin',
	language: 'en',
	simpleMode: false,
	labels: {
		gitBranch: 'Git Branch',
	},
	system: {
		memory: {
			usageMb: 186,
			formattedUsage: '186 MB',
		},
		modes: {
			yolo: false,
			plan: true,
			vulnerabilityHunting: false,
			toolSearchEnabled: true,
			hybridCompress: false,
			team: false,
			ultraTodo: false,
			telemetry: true,
			simple: false,
		},
		ide: {
			connectionStatus: 'connected',
			editorContext: {
				activeFile: '/path/to/file.ts',
				selectedText: 'const answer = 42;',
				cursorPosition: {line: 10, character: 5},
				workspaceFolder: '/path/to/workspace',
			},
			selectedTextLength: 18,
		},
		backend: {
			connectionStatus: 'connected',
			instanceName: 'default',
		},
		contextWindow: {
			inputTokens: 18234,
			maxContextTokens: 128000,
			cacheCreationTokens: 2048,
			cacheReadTokens: 8192,
			percentage: 22.3,
			totalInputTokens: 28474,
			hasAnthropicCache: true,
			hasOpenAICache: false,
			hasAnyCache: true,
		},
		codebase: {
			indexing: true,
			progress: {
				totalFiles: 100,
				processedFiles: 42,
				totalChunks: 320,
				currentFile: 'source/app.ts',
				status: 'indexing',
			},
		},
		watcher: {
			enabled: true,
			fileUpdateNotification: {
				file: 'source/app.ts',
				timestamp: 1710000000000,
			},
		},
		clipboard: {
			text: 'Input copied',
			isError: false,
			timestamp: 1710000000000,
		},
		privacy: {
			configured: true,
			enabled: true,
			mode: 'api',
			apiUrlConfigured: true,
			apiUrl: 'https://privacy.example.com/v1/filter',
			model: 'openai/privacy-filter',
			toolResultTools: ['filesystem-read', 'terminal-execute'],
		},
		profile: {
			currentName: 'default',
			baseUrl: 'https://api.openai.com/v1',
			requestMethod: 'chat',
			advancedModel: 'gpt-4o',
			basicModel: 'gpt-4o-mini',
			maxContextTokens: 128000,
			maxTokens: 4096,
			anthropicBeta: false,
			anthropicCacheTTL: '5m',
			thinkingEnabled: false,
			thinkingType: 'adaptive',
			thinkingBudgetTokens: 4096,
			thinkingEffort: 'medium',
			geminiThinkingEnabled: false,
			geminiThinkingLevel: 'high',
			responsesReasoningEnabled: false,
			responsesReasoningEffort: 'medium',
			chatThinkingEnabled: false,
			chatReasoningEffort: 'high',
			responsesFastMode: false,
			responsesVerbosity: 'medium',
			anthropicSpeed: 'standard',
			enablePromptOptimization: true,
			enableAutoCompress: true,
			autoCompressThreshold: 80,
			showThinking: true,
			streamIdleTimeoutSec: 180,
			systemPromptId: ['default'],
			customHeadersSchemeId: 'default',
			toolResultTokenLimit: 100000,
			streamingDisplay: false,
		},
		compression: {
			blockToast: null,
		},
		speedometer: {
			enabled: true,
			tps: 42,
			peakTps: 58,
			ttftMs: 1234,
		},
	},
}
```

Field description:

- `cwd`: current Snow CLI working directory
- `platform`: current Node.js platform value, such as `darwin`, `linux`, `win32`
- `language`: current Snow CLI language, one of `en`, `zh`, `zh-TW`
- `simpleMode`: whether Snow CLI is in simple theme mode
- `labels`: localized labels that built-in plugins may reuse
- `system`: a ready-to-use snapshot of current StatusLine system state

Available fields under `system`:

- `system.memory`: current Snow CLI process memory, including `usageMb` and `formattedUsage`
- `system.modes`: current mode flags, including `yolo`, `plan`, `vulnerabilityHunting`, `toolSearchEnabled`, `hybridCompress`, `team`, `ultraTodo`, `telemetry`, `simple`
- `system.ide`: IDE connection state, including `connectionStatus`, `editorContext`, `selectedTextLength`
- `system.backend`: backend connection state, including `connectionStatus`, `instanceName`
- `system.contextWindow`: context window state; when present it includes token metrics, cache metrics, `percentage`, and `totalInputTokens`
- `system.codebase`: codebase indexing state, including `indexing` and `progress`
- `system.watcher`: file watcher state, including `enabled` and `fileUpdateNotification`
- `system.clipboard`: most recent copy feedback, including `text`, `isError`, `timestamp`
- `system.privacy`: privacy filter configuration snapshot, including `configured`, `enabled`, `mode`, `apiUrlConfigured`, `apiUrl`, `model`, and `toolResultTools`. This state is exposed only for plugins; Snow CLI itself does not render a default Privacy item in StatusLine.
  - `apiUrlConfigured` is a compatibility boolean that only indicates whether an API endpoint is configured.
  - `apiUrl` is the actual privacy filter API request URL; it is returned only when `mode` is `api` and the endpoint is non-empty. In `local` mode or without an endpoint, it is `undefined`.
- `system.profile`: current profile full configuration, including `currentName`, `baseUrl`, `requestMethod`, `advancedModel`, `basicModel`, `maxContextTokens`, `maxTokens`, `anthropicBeta`, `anthropicCacheTTL`, `thinkingEnabled`, `thinkingType`, `thinkingBudgetTokens`, `thinkingEffort`, `geminiThinkingEnabled`, `geminiThinkingLevel`, `responsesReasoningEnabled`, `responsesReasoningEffort`, `chatThinkingEnabled`, `chatReasoningEffort`, `responsesFastMode`, `responsesVerbosity`, `anthropicSpeed`, `enablePromptOptimization`, `enableAutoCompress`, `autoCompressThreshold`, `showThinking`, `streamIdleTimeoutSec`, `systemPromptId`, `customHeadersSchemeId`, `toolResultTokenLimit`, `streamingDisplay` (excluding `apiKey`)

  How thinking / reasoning fields map to `requestMethod`:

  - `anthropic`: `thinkingEnabled` / `thinkingType` (`'enabled'` or `'adaptive'`) / `thinkingEffort` / `thinkingBudgetTokens`
  - `gemini`: `geminiThinkingEnabled` / `geminiThinkingLevel`
  - `responses`: `responsesReasoningEnabled` / `responsesReasoningEffort`
  - `chat` (OpenAI Chat Completions compatible APIs such as DeepSeek): `chatThinkingEnabled` / `chatReasoningEffort`

- `system.compression`: auto-compression state, including `blockToast`
- `system.speedometer`: real-time speedometer state, including `enabled` (whether enabled), `tps` (current tokens/s), `peakTps` (peak tokens/s), `ttftMs` (Time To First Token in milliseconds; `null` means no first token received yet in the current session). `enabled` is `true` only when the `/speedometer` command is active.

## Example 1: Real Clock Plugin

A real working example file already exists in your user directory:

````bash
~/.snow/plugin/statusline/example-clock.js


Its content is:

```js
const messages = {
	en: {
		label: 'Current Time',
		directory: 'Directory',
	},
	zh: {
		label: '当前时间',
		directory: '目录',
	},
	'zh-TW': {
		label: '當前時間',
		directory: '目錄',
	},
};

export default {
	id: 'custom.example-clock',
	refreshIntervalMs: 60_000,
	getItems(context) {
		const now = new Date();
		const hours = String(now.getHours()).padStart(2, '0');
		const minutes = String(now.getMinutes()).padStart(2, '0');
		const clock = `${hours}:${minutes}`;
		const message = messages[context.language] || messages.en;

		return {
			id: 'custom-example-clock',
			text: `◷ ${clock}`,
			detailedText: `◷ ${message.label}: ${clock} · ${message.directory}: ${context.cwd}`,
			color: '#A78BFA',
			priority: 200,
		};
	},
};
````

## Example 2: Show Current Directory Name

```js
import path from 'node:path';

export default {
	id: 'custom.cwd-name',
	refreshIntervalMs: 5000,
	getItems(context) {
		const folderName = path.basename(context.cwd);
		return {
			text: `DIR ${folderName}`,
			detailedText: `Current Folder: ${folderName}`,
			color: 'green',
			priority: 150,
		};
	},
};
```

## Example 3: Use System State

```js
export default {
	id: 'custom.system-status',
	refreshIntervalMs: 3000,
	getItems(context) {
		const items = [];

		if (context.system.ide.connectionStatus === 'connected') {
			const activeFile = context.system.ide.editorContext?.activeFile;
			items.push({
				id: 'custom-system-ide',
				text: activeFile ? 'IDE ON' : 'IDE READY',
				detailedText: activeFile
					? `IDE connected · Active file: ${activeFile}`
					: 'IDE connected',
				color: '#22C55E',
				priority: 120,
			});
		}

		if (context.system.contextWindow) {
			items.push({
				id: 'custom-system-context',
				text: `CTX ${context.system.contextWindow.percentage.toFixed(1)}%`,
				detailedText: `Context used: ${context.system.contextWindow.totalInputTokens} tokens`,
				color: 'cyan',
				priority: 130,
			});
		}

		items.push({
			id: 'custom-system-memory',
			text: `MEM ${context.system.memory.formattedUsage}`,
			detailedText: `Current memory usage: ${context.system.memory.formattedUsage}`,
			color: 'yellow',
			priority: 140,
		});

		return items;
	},
};
```

## Example 4: Return Multiple Status Items

```js
export default {
	id: 'custom.multi-status',
	refreshIntervalMs: 30000,
	getItems() {
		const now = new Date();
		return [
			{
				text: `T ${String(now.getHours()).padStart(2, '0')}:${String(
					now.getMinutes(),
				).padStart(2, '0')}`,
				color: 'cyan',
				priority: 100,
			},
			{
				text: 'ENV DEV',
				detailedText: 'Environment: Development',
				color: 'yellow',
				priority: 110,
			},
		];
	},
};
```

## Example 5: Gradient Colors

Use the `gradient` field to apply gradient colors to a status item. Snow CLI automatically interpolates a per-character color sequence based on the text length:

```js
export default {
	id: 'custom.gradient-status',
	refreshIntervalMs: 5000,
	getItems(context) {
		const items = [];

		// Two-color gradient: green to blue
		items.push({
			id: 'gradient-green-blue',
			text: `⚑ ${context.system.profile?.currentName || 'default'}`,
			detailedText: `Current Profile: ${
				context.system.profile?.currentName || 'default'
			}`,
			gradient: ['#10B981', '#60A5FA'],
			priority: 100,
		});

		// Three-color gradient: purple to pink to orange
		items.push({
			id: 'gradient-purple-pink-orange',
			text: '★ PREMIUM',
			detailedText: 'Premium membership status',
			gradient: ['#A78BFA', '#F472B6', '#FB923C'],
			priority: 110,
		});

		return items;
	},
};
```

## Built-in Git Branch Example

Snow CLI already includes a built-in Git branch StatusLine plugin.

Reference implementation:

- `source/ui/components/common/statusline/gitBranch.ts`

This built-in hook:

- uses hook id `builtin.git-branch`
- refreshes every 10 seconds
- reads the current Git branch from `context.cwd`
- renders short and detailed text separately

If you create another plugin with the same hook id, you can override the built-in behavior.

## Built-in Hook IDs (Overridable)

In addition to `builtin.git-branch`, Snow CLI reserves stable hook ids for all
other built-in status items. If your plugin registers a hook with one of these
ids, Snow CLI will skip the hard-coded rendering for that item and use your
hook's output instead:

| Hook ID                      | Default Rendering                               | Trigger Condition                                                                                |
| ---------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `builtin.profile`            | `§ {profileName}`                               | A current profile is active                                                                      |
| `builtin.mode-yolo`          | `⧴ YOLO`                                        | YOLO mode is enabled                                                                             |
| `builtin.mode-plan`          | `⚐ Plan`                                        | Plan mode is enabled                                                                             |
| `builtin.mode-hunt`          | `⍨ Vuln Hunt`                                   | Vulnerability hunting mode is enabled                                                            |
| `builtin.mode-team`          | `⚑ Team`                                        | Team mode is enabled                                                                             |
| `builtin.mode-ultra-todo`    | `◈ Ultra TODO`                                  | Ultra TODO mode is enabled                                                                       |
| `builtin.tool-search`        | `♾︎ ToolSearch ON`                              | On-demand tool search is enabled                                                                 |
| `builtin.hybrid-compress`    | `⇌ Hybrid Compress`                             | Hybrid compression is enabled                                                                    |
| `builtin.telemetry`          | `⌁ OTel`                                        | Telemetry is enabled                                                                             |
| `builtin.privacy`            | No default rendering; plugin-only               | Readable whether privacy is configured; API mode exposes `system.privacy.apiUrl` when configured |
| `builtin.ide-connection`     | `◐/●/○ IDE`                                     | VSCode connection is not disconnected                                                            |
| `builtin.backend-connection` | `◐/↻/● Backend`                                 | Backend connection is not disconnected                                                           |
| `builtin.codebase-indexing`  | `◐ Indexing {processed}/{total}` or error       | Indexing is running or has errored                                                               |
| `builtin.watcher`            | `☉ Watcher`                                     | Watcher is enabled and not indexing                                                              |
| `builtin.file-update`        | `⛁ Updated`                                     | A file update notification arrived                                                               |
| `builtin.copy-status`        | Clipboard success / failure toast               | A clipboard message exists                                                                       |
| `builtin.compress-block`     | Auto-compression block toast                    | Auto compression was blocked                                                                     |
| `builtin.memory`             | `⛁ {memoryUsage}`                               | Always rendered                                                                                  |
| `builtin.speedometer`        | `⏱ {tps} tok/s · peak {peakTps} · ttft {ttft}s` | Speedometer is enabled (via `/speedometer` command)                                              |
| `builtin.git-branch`         | `⑂ {branch}`                                    | The current directory is a Git repo                                                              |

Notes:

- Once a plugin registers a hook with the same id, Snow CLI completely skips
  the corresponding built-in render. Icons, colors, thresholds, etc. are then
  fully controlled by your hook.
- Whether the built-in item is visible at all (e.g. whether YOLO mode is on) is
  still determined by Snow CLI. Your plugin can read the same flags via
  `context.system.modes`, `context.system.ide`, `context.system.contextWindow`,
  etc. to decide what to return.
- Overriding `builtin.memory` removes the default `⛁ 232 MB` block, so make
  sure your hook renders memory information (you can read
  `context.system.memory.usageMb`).

## Override Example

```js
export default {
	id: 'builtin.git-branch',
	refreshIntervalMs: 15000,
	async getItems(context) {
		return {
			text: '⑂ custom-branch',
			detailedText: `⑂ Custom Git Branch (${context.cwd})`,
			color: 'magenta',
			priority: 100,
		};
	},
};
```

## Error Handling

If a plugin fails:

- Snow CLI skips the broken result for that refresh cycle
- the error is written to the Snow CLI log
- other plugins continue to run

Common problems:

- file is not in `~/.snow/plugin/statusline/`

- file extension is not supported
- exported value is not a valid hook object
- `text` is missing or empty
- plugin code throws at runtime

## Best Practices

- Keep `getItems()` fast and lightweight
- Use a reasonable refresh interval
- Return `undefined` when the status should be hidden
- Use stable `id` values for predictable ordering and override behavior
- Prefer `detailedText` for verbose mode and `text` for compact mode
- Plugin file changes hot-reload automatically — no restart needed

## Troubleshooting

### Plugin does not appear

Check:

1. File path is `~/.snow/plugin/statusline/*.js`
2. Wait a moment for hot-reload (or save the file again)
3. Export format is valid
4. `text` is not empty
5. The plugin does not throw errors during execution

### Status order is unexpected

Check `priority` values.

- smaller number = earlier render
- larger number = later render

### My plugin does not override built-in Git branch

Make sure the hook `id` exactly matches:

```js
id: 'builtin.git-branch';
```

## Related Files

- `source/ui/components/common/statusline/useStatusLineHooks.ts`
- `source/ui/components/common/statusline/types.ts`
- `source/ui/components/common/statusline/gitBranch.ts`
- `~/.snow/plugin/statusline/example-clock.js`
